package handler

import (
	"database/sql"
	"fmt"
	"gitee.com/scottq/go-framework/src/miscs"
	"os"
	"time"
)

const LOCK_TABLE = "lock_optimistic"

var CREATE_LOCK_TABLE_SQL = fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS  %s
(
	id        bigint unsigned                         NOT NULL AUTO_INCREMENT,
	lock_name varchar(64) COLLATE utf8mb4_unicode_ci  NOT NULL DEFAULT '' COMMENT '锁名',
	lock_at datetime DEFAULT NULL COMMENT '锁定时间',
	unlock_at datetime DEFAULT NULL COMMENT '解锁时间',
    lock_host varchar(32) COLLATE utf8mb4_unicode_ci  NOT NULL DEFAULT '' COMMENT '主机信息',
	timeout bigint unsigned default 0 COMMENT '超时时间，单位：秒',
    status int unsigned DEFAULT 0 COMMENT '锁状态,0:未锁定,1:已锁',

    PRIMARY KEY (id),
    UNIQUE KEY idx_lock (lock_name)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT ='乐观锁表'
`, LOCK_TABLE)

type IOptimisticLock interface {
	Lock(name string) bool
	UnLock(name string) bool
}

type OptimisticLock struct {
	db      *sql.DB
	timeout int64 //unit: seconds
}

func NewOptimisticLock(db *sql.DB, timeout int64) (IOptimisticLock, error) {
	lock := &OptimisticLock{
		db:      db,
		timeout: timeout,
	}

	if err := lock.init(); err != nil {
		return nil, err
	}

	return lock, nil
}

type Lock struct {
	LockName string
	LockAt   string
	UnLockAt string
	LockHost string
	Timeout  int64
	Status   int
}

func (l *Lock) IsLock() bool {
	lockTime, err := time.Parse(miscs.DateTimeLayout, l.LockAt)
	if err != nil {
		return false
	}
	expireTime := lockTime.Add(time.Second * time.Duration(l.Timeout))

	if l.Status == STATUS_LOCK && expireTime.UnixNano() > time.Now().UnixNano() {
		//锁中
		return true
	}

	//未锁
	return false
}

const STATUS_UNLOCK = 0
const STATUS_LOCK = 1

func (this *OptimisticLock) Lock(name string) bool {
	findLock, err := this.queryLock(name)
	if err != nil {
		fmt.Println("query lock fail:", err)
		return false
	}
	if findLock == nil {
		err := this.addLock(name)
		if err != nil {
			fmt.Println("add lock fail:", err)
			return false
		}
		findLock, err = this.queryLock(name)
		if err != nil {
			fmt.Println("after add query lock fail:", err)
			return false
		}
	} else if findLock.IsLock() {
		fmt.Println("query is lock fail:", err)
		return false
	}

	lock, err := this.lockLock(name, findLock.LockAt)
	if err != nil {
		fmt.Println("lock fail:", err)
		return false
	}

	return lock
}

func (this *OptimisticLock) UnLock(name string) bool {
	ok, err := this.unLock(name)
	if err != nil {
		fmt.Println(err)
		return false
	}
	return ok
}

func (this *OptimisticLock) init() error {
	var err error

	_, err = this.db.Exec(CREATE_LOCK_TABLE_SQL)
	if err != nil {
		return err
	}

	return nil
}

func (this *OptimisticLock) queryLock(name string) (*Lock, error) {
	db := this.db

	findLock := &Lock{}

	selectSql := fmt.Sprintf("SELECT lock_name,ifnull(lock_at,'') as lock_at,lock_host,timeout,status FROM %s WHERE lock_name=? LIMIT 1", LOCK_TABLE)
	row := db.QueryRow(selectSql, name)
	err := row.Scan(
		&findLock.LockName,
		&findLock.LockAt, &findLock.LockHost,
		&findLock.Timeout, &findLock.Status)
	if err == sql.ErrNoRows {
		return nil, nil
	} else if err != nil {
		return nil, err
	}

	return findLock, nil
}

func (this *OptimisticLock) addLock(name string) error {
	db := this.db

	t := time.Unix(0, 0).Format(miscs.DateTimeLayout)
	insertSql := fmt.Sprintf("INSERT INTO %s SET lock_name=?,status=?,lock_at=?,unlock_at=?", LOCK_TABLE)
	_, err := db.Exec(insertSql, name, STATUS_UNLOCK, t, t)

	if err != nil {
		return err
	}
	return nil
}

func (this *OptimisticLock) lockLock(name string, lastLockAt string) (bool, error) {
	db := this.db

	lockAt := time.Now().Format(miscs.DateTimeLayout)
	hostname, err := os.Hostname()

	updateSql := fmt.Sprintf("UPDATE %s SET lock_at=?,lock_host=?,timeout=?,status=? WHERE lock_name=? AND lock_at=?", LOCK_TABLE)

	ret, err := db.Exec(updateSql,
		lockAt, hostname, this.timeout, STATUS_LOCK,
		name, lastLockAt)
	if err != nil {
		return false, err
	}

	a, _ := ret.RowsAffected()

	return a == 1, nil
}

func (this *OptimisticLock) unLock(name string) (bool, error) {
	db := this.db
	unlockAt := time.Now().Format(miscs.DateTimeLayout)

	updateSql := fmt.Sprintf("UPDATE %s SET unlock_at=?,status=? WHERE lock_name=? AND status=?", LOCK_TABLE)

	ret, err := db.Exec(updateSql, unlockAt, STATUS_UNLOCK, name, STATUS_LOCK)
	if err != nil {
		return false, err
	}

	a, _ := ret.RowsAffected()

	return a == 1, nil
}
